<?php

/* ========================================================================= */
/* This file is part of the HypnoPHP Framework Library                       */
/* Copyright (c) 2009-2010, Radu Buzea                                       */
/* ========================================================================= */

/**
 @author Alexandre Tedeschi (alexandrebr at gmail dot com)
 @license GNU Lesser General Public License (www.gnu.org/copyleft/lesser.html)
 @package HypnoPHP

*/

defined('HYPNOPHP_APPLICATION') or
die("Cannot use <i>" . basename(__FILE__) . "</i> outside HypnoPHP Application");

// ============================================================================
// Class: HUnZip
// ============================================================================

class HUnZip extends HCustomFile
{

	protected $__compressed_list = array();
	protected $__set_dir_list = array();
	protected $__end_of_set_dir = array();
	protected $__last_backup_dir = null;

	const ZIP_SIGNATURE = "\x50\x4b\x03\x04";
	const ZIP_SET_SIGNATURE = "\x50\x4b\x01\x02";
	const ZIP_END_OF_SET_SIGNATURE = "\x50\x4b\x05\x06";
	const BACKUP_DIR_FORMAT = "Ymd-His";

	# ###########################################################################

	/**
	* Class construct
	*
	* @return HUnZip
	*/
	public function __construct($file_name = '')
	{
		parent::__construct();
		$this->__compressed_list = array();
		$this->__set_dir_list = array();
		$this->__end_of_set_dir = array();
		$this->open($file_name, "r");
	}

	# ###########################################################################

	/**
	* Get the list of files and folders stored in the archive file
	*
	* @param bool $stop_on_file
	* @return array or false on error
	*/
	public function get_list($stop_on_file = false)
	{
		if (!$this->__file_handle) {
			if (!$this->open($this->__file_name)) {
				$this->__putMessage(E_USER_WARNING, "Cannot get the archive content.");
				return false;
			}
		}
		$compressed_list = $this->__compressed_list;
		if (is_array($compressed_list) && sizeof($compressed_list) > 0) {
			$this->__putMessage(H_VERBOSE_MESSAGE, 'File list already loaded...');
			return $compressed_list;
		}
		if (!$this->__load_file_list_EOF($this->__file_handle, $stop_on_file)) {
			$this->__putMessage(H_VERBOSE_MESSAGE, "Failed trying to load the archive content. Looking for signatures...");
			if (!$this->__load_file_list_signature($this->__file_handle, $stop_on_file)) {
				$this->__putMessage(E_USER_WARNING, "Could not find any valid header. The archive file is corrupted or empty.");
				return false;
			}
		}
		return $this->__compressed_list;
	}

	# ###########################################################################

	/**
	* Get extra info about a file or folder stored in the ZIP file
	*
	* @param string $compressed_file_fame
	* @return string
	*/
	public function get_extra_info($compressed_file_fame = '')
	{
		return isset($this->__set_dir_list[$compressed_file_fame]) ? $this->__set_dir_list[$compressed_file_fame] : false;
	}

	# ###########################################################################

	/**
	* Get info about the ZIP file
	*
	* @param bool $detail
	* @return string
	*/
	public function get_zip_info($detail = false)
	{
		return $detail ? $this->__end_of_set_dir[$detail] : $this->__end_of_set_dir;
	}

	# ###########################################################################

	/**
	* Unzip a file from the ZIP
	*
	* @param string $compressed_file_name
	* @param string $target_file_name
	* @param int $apply_chmod (optional)
	* @return mixed
	*/
	public function unzip($compressed_file_name = '', $target_file_name = '', $apply_chmod = 0)
	{
		$compressed_list = $this->__compressed_list;
		if (!is_array($compressed_list) || sizeof($compressed_list) == 0) {
			$this->__putMessage(H_VERBOSE_MESSAGE, "Trying to unzip before loading the files list. Loading the ZIP list content...");
			$compressed_list = $this->get_list(false);//, $compressed_file_name);
			if (!is_array($compressed_list) || sizeof($compressed_list) == 0) {
				$this->__putMessage(E_USER_WARNING, "Cannot unzip file <i>{$compressed_file_name}</i>. Invalid ZIP list content.");
			}
		}
		$separator = DS;
		if ($separator == "/") {
			$target_file_name = str_replace("\\",  $separator, $target_file_name);
		} else {
			$target_file_name = str_replace("/", $separator, $target_file_name);
		}

		$file_details = &$compressed_list[$compressed_file_name];
		if (!isset($compressed_list[$compressed_file_name])) {
			$this->__putMessage(E_USER_WARNING, "The file <i>{$compressed_file_name}</i> is not in the ZIP list content.");
			return false;
		}
		if (substr($compressed_file_name, -1) == "/") {
			$this->__putMessage(E_USER_WARNING, "Trying to unzip a directory named <i>{$compressed_file_name}</i>.");
			return false;
		}
		if (!$file_details['uncompressed_size']) {
			$this->__putMessage(H_VERBOSE_MESSAGE, "The file <i>{$compressed_file_name}</i> is empty.");
			return $target_file_name ? file_put_contents($target_file_name, "") : "";
		}
		fseek($this->__file_handle, $file_details['contents-startOffset']);
		$result = $this->__uncompress(
			fread($this->__file_handle, $file_details['compressed_size']),
			$file_details['compression_method'],
			$file_details['uncompressed_size'],
			$target_file_name
		);
		if ($apply_chmod > 0 && $target_file_name != '' && is_file($target_file_name)) {
			chmod($target_file_name, $apply_chmod);
		}
		return $result;
	}

	# ###########################################################################

	/**
	* Unzip all files from the ZIP
	*
	* @param string $target_dir
	* @param string $target_file_name
	* @param bool $maintain_structure
	* @param int $apply_chmod
	* @return void
	*/
	public function unzip_all($target_dir = false, $base_dir = "", $maintain_structure = true, $apply_chmod = 0777)
	{
		$mkdir_chmod = 0777;
		if ($apply_chmod > 0) {
			$mkdir_chmod = $apply_chmod;
		}
		if ($target_dir === false) {
			$target_dir = dirname($_SERVER['SCRIPT_FILENAME']) . DS;
		}
		if(substr($target_dir, -1) != DS) {
			$target_dir .= DS;
		}
		if (!is_dir($target_dir)) {
			HFileSys::mkdir($target_dir, $mkdir_chmod);
		}
		if (!is_dir($target_dir) || !is_writable($target_dir)) {
			$this->__putMessage(E_USER_WARNING, "Cannot find the target directory <i>{$target_dir}</i>.");
			return;
		}
		if (substr($target_dir, -1, 1) == DS) {
			$target_dir = substr($target_dir, 0, -1);
		}

		$zip_content = $this->get_list();
		if (is_array($zip_content) && sizeof($zip_content) > 0) {
			foreach($zip_content as $file_name => $file_inf) {
				$dir_name = dirname($file_name);

				$target_file_dir = $target_dir . DS . $dir_name;
				if (substr($dir_name, 0, strlen($base_dir)) != $base_dir) {
					continue;
				}

				if (!is_dir($target_file_dir) && $maintain_structure){
					HFileSys::mkdir($target_file_dir, $mkdir_chmod);
				}
				if (substr($file_name, -1, 1) == "/") {
					continue;
				}
				if ($maintain_structure) {
					$target_file_name = $target_dir . DS . $file_name;
				} else {
					$target_file_name = $target_dir . DS . basename($file_name);
				}
				$this->unzip($file_name, $target_file_name, $apply_chmod);
			}
		}
	}

	# ###########################################################################

	/**
	* Internaly load the files list from the ZIP file
	*
	* @param int $file_handle
	* @param bool $stop_on_file
	* @return bool
	*/
	protected function __load_file_list_EOF(&$file_handle, $stop_on_file = false)
	{
		for ($x = 0; $x < 1024; $x++) {
			fseek($file_handle, -22 - $x, SEEK_END);
			$signature = fread($file_handle, 4);
			if ($signature == self::ZIP_END_OF_SET_SIGNATURE) {
				$eodir['disk_number_this'] = unpack("v", fread($file_handle, 2));
				$eodir['disk_number'] = unpack("v", fread($file_handle, 2));
				$eodir['total_entries_this'] = unpack("v", fread($file_handle, 2));
				$eodir['total_entries'] = unpack("v", fread($file_handle, 2));
				$eodir['size_of_cd'] = unpack("V", fread($file_handle, 4));
				$eodir['offset_start_cd'] = unpack("V", fread($file_handle, 4));
				$zipFileCommentLenght = unpack("v", fread($file_handle, 2));
				$eodir['zipfile_comment'] = $zipFileCommentLenght[1] ? fread($file_handle, $zipFileCommentLenght[1]) : '';
				$this->__end_of_set_dir = Array (
					'disk_number_this' => $eodir['disk_number_this'][1],
					'disk_number' => $eodir['disk_number'][1],
					'total_entries_this' => $eodir['total_entries_this'][1],
					'total_entries' => $eodir['total_entries'][1],
					'size_of_cd' => $eodir['size_of_cd'][1],
					'offset_start_cd' => $eodir['offset_start_cd'][1],
					'zipfile_comment' => $eodir['zipfile_comment'],
				);
				fseek($file_handle, $this->__end_of_set_dir['offset_start_cd']);
				$signature = fread($file_handle, 4);
				while ($signature == self::ZIP_SET_SIGNATURE) {
					$dir['version_madeby'] = unpack("v", fread($file_handle, 2));
					$dir['version_needed'] = unpack("v", fread($file_handle, 2));
					$dir['general_bit_flag'] = unpack("v", fread($file_handle, 2));
					$dir['compression_method'] = unpack("v", fread($file_handle, 2));
					$dir['lastmod_time'] = unpack("v", fread($file_handle, 2));
					$dir['lastmod_date'] = unpack("v", fread($file_handle, 2));
					$dir['crc-32'] = fread($file_handle, 4);
					$dir['compressed_size'] = unpack("V", fread($file_handle, 4));
					$dir['uncompressed_size'] = unpack("V", fread($file_handle, 4));
					$file_nameLength = unpack("v", fread($file_handle, 2));
					$extraFieldLength = unpack("v", fread($file_handle, 2));
					$fileCommentLength = unpack("v", fread($file_handle, 2));
					$dir['disk_number_start'] = unpack("v", fread($file_handle, 2));
					$dir['internal_attributes'] = unpack("v", fread($file_handle, 2));
					$dir['external_attributes1'] = unpack("v", fread($file_handle, 2));
					$dir['external_attributes2'] = unpack("v", fread($file_handle, 2));
					$dir['relative_offset'] = unpack("V", fread($file_handle, 4));
					$dir['file_name'] = fread($file_handle, $file_nameLength[1]);
					$dir['extra_field'] = $extraFieldLength[1] ? fread($file_handle, $extraFieldLength[1]) : '';
					$dir['file_comment'] = $fileCommentLength[1] ? fread($file_handle, $fileCommentLength[1]) : '';
					$BINlastmod_date = str_pad(decbin($dir['lastmod_date'][1]), 16, '0', STR_PAD_LEFT);
					$BINlastmod_time = str_pad(decbin($dir['lastmod_time'][1]), 16, '0', STR_PAD_LEFT);
					$lastmod_dateY = bindec(substr($BINlastmod_date, 0, 7)) + 1980;
					$lastmod_dateM = bindec(substr($BINlastmod_date, 7, 4));
					$lastmod_dateD = bindec(substr($BINlastmod_date, 11, 5));
					$lastmod_timeH = bindec(substr($BINlastmod_time, 0, 5));
					$lastmod_timeM = bindec(substr($BINlastmod_time, 5, 6));
					$lastmod_timeS = bindec(substr($BINlastmod_time, 11, 5));
					$this->__set_dir_list[$dir['file_name']] = Array (
						'version_madeby' => $dir['version_madeby'][1],
						'version_needed' => $dir['version_needed'][1],
						'general_bit_flag' => str_pad(decbin($dir['general_bit_flag'][1]), 8, '0', STR_PAD_LEFT),
						'compression_method' => $dir['compression_method'][1],
						'lastmod_datetime' => mktime(
							$lastmod_timeH,
							$lastmod_timeM,
							$lastmod_timeS,
							$lastmod_dateM,
							$lastmod_dateD,
							$lastmod_dateY
						),
						'crc-32' => str_pad(dechex(ord($dir['crc-32'][3])), 2, '0', STR_PAD_LEFT) .
							str_pad(dechex(ord($dir['crc-32'][2])), 2, '0', STR_PAD_LEFT) .
							str_pad(dechex(ord($dir['crc-32'][1])), 2, '0', STR_PAD_LEFT) .
							str_pad(dechex(ord($dir['crc-32'][0])), 2, '0', STR_PAD_LEFT),
						'compressed_size' => $dir['compressed_size'][1],
						'uncompressed_size' => $dir['uncompressed_size'][1],
						'disk_number_start' => $dir['disk_number_start'][1],
						'internal_attributes' => $dir['internal_attributes'][1],
						'external_attributes1' => $dir['external_attributes1'][1],
						'external_attributes2' => $dir['external_attributes2'][1],
						'relative_offset' => $dir['relative_offset'][1],
						'file_name' => $dir['file_name'],
						'extra_field' => $dir['extra_field'],
						'file_comment' => $dir['file_comment'],
					);
					$signature = fread($file_handle, 4);
				}
				if ($this->__set_dir_list) {
					foreach ($this->__set_dir_list as $filename => $details) {
						$i = $this->__get_file_header_info($file_handle, $details['relative_offset']);
						$this->__compressed_list[$filename]['file_name'] = $filename;
						$this->__compressed_list[$filename]['compression_method'] = $details['compression_method'];
						$this->__compressed_list[$filename]['version_needed'] = $details['version_needed'];
						$this->__compressed_list[$filename]['lastmod_datetime'] = $details['lastmod_datetime'];
						$this->__compressed_list[$filename]['crc-32'] = $details['crc-32'];
						$this->__compressed_list[$filename]['compressed_size'] = $details['compressed_size'];
						$this->__compressed_list[$filename]['uncompressed_size'] = $details['uncompressed_size'];
						$this->__compressed_list[$filename]['lastmod_datetime'] = $details['lastmod_datetime'];
						$this->__compressed_list[$filename]['extra_field'] = $i['extra_field'];
						$this->__compressed_list[$filename]['contents-startOffset'] = $i['contents-startOffset'];
						if (strtolower($stop_on_file) == strtolower($filename)) { break; }
					}
				}
				return true;
			}
		}
		return false;
	}

	# ###########################################################################

	/**
	* Internaly load the files signature list from the ZIP file
	*
	* @param int $file_handle
	* @param bool $stop_on_file
	* @return bool
	*/
	protected function __load_file_list_signature(&$file_handle, $stop_on_file = false)
	{
		fseek($file_handle, 0);
		$return = false;
		for (;;) {
			$details = $this->__get_file_header_info($file_handle);
			if (!$details) {
				$this->__putMessage(H_VERBOSE_MESSAGE, "Invalid signature. Trying to verify if is old style Data Descriptor...");
				fseek($file_handle, 12 - 4, SEEK_CUR);
				$details = $this->__get_file_header_info($file_handle);
			}
			if (!$details) {
				$this->__putMessage(H_VERBOSE_MESSAGE, "Still invalid signature. Probably reached the end of the file.");
				break;
			}
			$filename = $details['file_name'];
			$this->__compressed_list[$filename] = $details;
			$return = true;
			if (strtolower($stop_on_file) == strtolower($filename)) {
				break;
			}
		}
		return $return;
	}

	# ###########################################################################

	/**
	* Internaly get file header info from the ZIP file
	*
	* @param int $file_handle
	* @param int or bool $start_offset
	* @return bool
	*/
	protected function __get_file_header_info(&$file_handle, $start_offset = false)
	{
		if ($start_offset !== false) {
			fseek($file_handle, $start_offset);
		}
		$signature = fread($file_handle, 4);
		if ($signature == self::ZIP_SIGNATURE) {
			$file['version_needed'] = unpack("v", fread($file_handle, 2));
			$file['general_bit_flag'] = unpack("v", fread($file_handle, 2));
			$file['compression_method'] = unpack("v", fread($file_handle, 2));
			$file['lastmod_time'] = unpack("v", fread($file_handle, 2));
			$file['lastmod_date'] = unpack("v", fread($file_handle, 2));
			$file['crc-32'] = fread($file_handle, 4);
			$file['compressed_size'] = unpack("V", fread($file_handle, 4));
			$file['uncompressed_size'] = unpack("V", fread($file_handle, 4));
			$file_nameLength = unpack("v", fread($file_handle, 2));
			$extraFieldLength = unpack("v", fread($file_handle, 2));
			$file['file_name'] = fread($file_handle, $file_nameLength[1]);
			$file['extra_field'] = $extraFieldLength[1] ? fread($file_handle, $extraFieldLength[1]) : '';
			$file['contents-startOffset'] = ftell($file_handle);
			fseek($file_handle, $file['compressed_size'][1], SEEK_CUR);
			$BINlastmod_date = str_pad(decbin($file['lastmod_date'][1]), 16, '0', STR_PAD_LEFT);
			$BINlastmod_time = str_pad(decbin($file['lastmod_time'][1]), 16, '0', STR_PAD_LEFT);
			$lastmod_dateY = bindec(substr($BINlastmod_date, 0, 7)) + 1980;
			$lastmod_dateM = bindec(substr($BINlastmod_date, 7, 4));
			$lastmod_dateD = bindec(substr($BINlastmod_date, 11, 5));
			$lastmod_timeH = bindec(substr($BINlastmod_time, 0, 5));
			$lastmod_timeM = bindec(substr($BINlastmod_time, 5, 6));
			$lastmod_timeS = bindec(substr($BINlastmod_time, 11, 5));
			$result = Array (
				'file_name' => $file['file_name'],
				'compression_method' => $file['compression_method'][1],
				'version_needed' => $file['version_needed'][1],
				'lastmod_datetime' => mktime(
					$lastmod_timeH,
					$lastmod_timeM,
					$lastmod_timeS,
					$lastmod_dateM,
					$lastmod_dateD,
					$lastmod_dateY
				),
				'crc-32' => str_pad(dechex(ord($file['crc-32'][3])), 2, '0', STR_PAD_LEFT) .
					str_pad(dechex(ord($file['crc-32'][2])), 2, '0', STR_PAD_LEFT) .
					str_pad(dechex(ord($file['crc-32'][1])), 2, '0', STR_PAD_LEFT) .
					str_pad(dechex(ord($file['crc-32'][0])), 2, '0', STR_PAD_LEFT),
				'compressed_size' => $file['compressed_size'][1],
				'uncompressed_size' => $file['uncompressed_size'][1],
				'extra_field' => $file['extra_field'],
				'general_bit_flag' => str_pad(decbin($file['general_bit_flag'][1]), 8, '0', STR_PAD_LEFT),
				'contents-startOffset' => $file['contents-startOffset'],
			);
			return $result;
		}
		return false;
	}

	# ###########################################################################

	/**
	* Internaly backup existing target files
	*
	* @param mixed $target_file_name
	* @return bool
	*/
	protected function __backup_target_file($target_file_name = false)
	{
		$separator = DS;
		$last_backup_dir = false;
		if ($target_file_name == false || !is_file($target_file_name)) {
			return false;
		}
		$backup_dir = $this->__last_backup_dir;
		if ($backup_dir == null) {
			$backup_dir = $this->backup_dir;
			if ($backup_dir == null) {
				return false;
			}
			$last_backup_dir = true;
		}
		if ($last_backup_dir) {
			if ($separator == "/") {
				$backup_dir = str_replace("\\", $separator, $backup_dir);
			} else {
				$backup_dir = str_replace("/", $separator, $backup_dir);
			}
			if (substr($backup_dir, -1, 1) != $separator) {
				$backup_dir .= $separator;
			}
			$backup_dir = $backup_dir . date(self::BACKUP_DIR_FORMAT) . $separator;
			if (!is_dir($backup_dir)) {
				if (!HFileSys::mkdir($backup_dir, 0777)) {
					$this->__putMessage(E_USER_WARNING, "Cannot create the backup directory <i>{$backup_dir}</i>.");
					return false;
				}
			}
			$this->__last_backup_dir = $backup_dir;
		}

		$backup_file_name = $target_file_name;
		$win_path = strpos($backup_file_name, ":\\");
		if ($win_path) { $backup_file_name = substr($backup_file_name, 3); }
		$backup_file_name = $backup_dir . $backup_file_name;
		$backup_file_path = dirname($backup_file_name);
		if (!is_dir($backup_file_path)) {
			if (!HFileSys::mkdir($backup_file_path, 0777)) {
				$this->__putMessage(E_USER_WARNING, "Cannot create the backup sub-directory <i>{$backup_file_path}</i>.");
				return false;
			}
		}
		$result = copy($target_file_name, $backup_file_name);
		if ($result) {
			$this->__putMessage(H_VERBOSE_MESSAGE, "Created backup file <i>{$backup_file_name}</i>.");
		} else {
			$this->__putMessage(H_VERBOSE_MESSAGE, "Cannot create the backup file <i>{$backup_file_name}</i>.");
		}
		return $result;
	}

	# ###########################################################################

	/**
	* Internaly uncompress a file/content from the ZIP file
	*
	* @param string $content
	* @param int $mode
	* @param int $uncompressed_size
	* @param string $target_file_name
	* @return mixed
	*/
	protected function __uncompress($content, $mode, $uncompressed_size, $target_file_name = false)
	{
		switch ($mode) {
			case 0 : // Return uncompressed data
				$this->__backup_target_file($target_file_name);
				return $target_file_name ? file_put_contents($target_file_name, $content) : $content;
			case 1 :
				$this->__putMessage(E_USER_WARNING, "Shrunk mode is not supported.");
				return false;
			case 2 :
			case 3 :
			case 4 :
			case 5 :
				$cpfactor = $mode - 1;
				$this->__putMessage(E_USER_WARNING, "Compression factor {$cpfactor} is not supported.");
				return false;
			case 6 :
				$this->__putMessage(E_USER_WARNING, "Implode is not supported.");
				return false;
			case 7 :
				$this->__putMessage(E_USER_WARNING, "Tokenizing compression algorithm is not supported.");
				return false;
			case 8 : // Decompress Lempel-Ziv encoded data
				if ($target_file_name !== false) {
					$this->__putMessage(H_VERBOSE_MESSAGE, "Decompress Lempel-Ziv data to file <i>{$target_file_name}</i> ({$uncompressed_size})");
				} else {
					$this->__putMessage(H_VERBOSE_MESSAGE, "Decompress Lempel-Ziv data ({$uncompressed_size})");
				}
				$data = gzinflate($content, $uncompressed_size);
				$this->__backup_target_file($target_file_name);
				return $target_file_name ? file_put_contents($target_file_name, $data) : $data;
			case 9 :
				$this->__putMessage(E_USER_WARNING, "Enhanced Deflating is not supported.");
				return false;
			case 10 :
				$this->__putMessage(E_USER_WARNING, "PKWARE Date Compression Library Impoloding is not supported.");
				return false;
			case 12 : // Decompress BZIP2 encoded data
				if ($target_file_name !== false) {
					$this->__putMessage(H_VERBOSE_MESSAGE, "Decompress BZIP2 data to file <i>{$target_file_name}</i>.");
				} else {
					$this->__putMessage(H_VERBOSE_MESSAGE, "Decompress BZIP2 data");
				}
				$data = bzdecompress($content);
				$this->__backup_target_file($target_file_name);
				return $target_file_name ? file_put_contents($target_file_name, $data) : $data;
			case 18 :
				$this->__putMessage(E_USER_WARNING, "IBM TERSE is not supported.");
				return false;
			default :
				$this->__putMessage(E_USER_WARNING, "Unknown uncompress method #{$mode}.");
				return false;
		}
	}

	# ###########################################################################

}
